package com.mapforge.selection;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * The {@link DiscontinuousSelectionCollection} class represents a set of
 * selected points which may or may not be stored continuously.
 * 
 * @author Will Morrison
 * 
 */
public abstract class DiscontinuousSelectionCollection implements
		DiscontinuousSelection, Cloneable {
	/**
	 * The smallest rectangle that can contain every point in this selection
	 */
	private Rectangle boundingRectangle;

	/**
	 * Points considered within the selection.
	 */
	private Collection<ComparablePoint> selectedPoints;

	/**
	 * Reused to prevent unnecessary object creation when testing if a specific
	 * coordindate lies within this {@link Selection}.
	 * 
	 * @see {@link DiscontinuousSelectionCollection#contains(int, int)}
	 */
	private static final ComparablePoint NULLPOINT = new ComparablePoint();

	/**
	 * Constructs a new {@link DiscontinuousSelectionCollection} with selected
	 * points held in the specified {@link Collection Collection.} The specified
	 * data structure should not be manipulated elsewhere, as its contents
	 * cannot be manipulated outside this
	 * {@link DiscontinuousSelectionCollection}, else this
	 * {@link DiscontinuousSelectionCollection} will not function properly.
	 * 
	 * @param collection
	 *            specified collection this
	 *            {@link DiscontinuousSelectionCollection} will use.
	 */
	public DiscontinuousSelectionCollection(
			Collection<ComparablePoint> collection) {
		this.selectedPoints = collection;
		this.boundingRectangle = new Rectangle();
		updateBoundingRectangle();
	}

	@Override
	public void clearSelection() {
		boundingRectangle.setBounds(0, 0, 0, 0);
		selectedPoints.clear();
	}

	@Override
	public Rectangle getBoundingRectangle() {
		return (Rectangle) boundingRectangle.clone();
	}

	// TODO
	public void setOrigin(int x, int y) {
		Point prevOrigin = new Point(boundingRectangle.x, boundingRectangle.y);
		boundingRectangle.setLocation(x, y);
		ComparablePoint temp;
		Iterator<ComparablePoint> it = iterator();
		while (it.hasNext()) {
			temp = it.next();
			temp.x = temp.x - prevOrigin.x + x;
			temp.y = temp.y - prevOrigin.y + y;
		}
	}

	public void addPoint(ComparablePoint p) {
		if (selectedPoints.add(p)) {
			if (!boundingRectangle.isEmpty()) {
				int x = boundingRectangle.x;
				int y = boundingRectangle.y;
				if (p.x < boundingRectangle.x) {
					boundingRectangle.x = p.x;
					boundingRectangle.width = (x + boundingRectangle.width)
							- p.x;
				}
				if (p.y < boundingRectangle.y) {
					boundingRectangle.y = p.y;
					boundingRectangle.height = y + boundingRectangle.height
							- p.y;
				}
				if (p.x >= x + boundingRectangle.width) {
					boundingRectangle.width = p.x - boundingRectangle.x + 1;
				}
				if (p.y >= y + boundingRectangle.height) {
					boundingRectangle.height = p.y - boundingRectangle.y + 1;
				}
			} else {
				boundingRectangle.setLocation(p);
				boundingRectangle.setSize(1, 1);
			}
		}
	}

	/**
	 * 
	 * @param points
	 */
	public void setSelectedPoints(Iterator<ComparablePoint> iterator) {
		this.selectedPoints.clear();
		while (iterator.hasNext()) {
			this.selectedPoints.add(iterator.next());
		}
		this.updateBoundingRectangle();
	}

	/**
	 * Recalculates the bounding rectangle.
	 */
	private final void updateBoundingRectangle() {
		boundingRectangle.setBounds(Integer.MAX_VALUE, Integer.MAX_VALUE,
				Integer.MIN_VALUE, Integer.MIN_VALUE);
		Iterator<ComparablePoint> it = selectedPoints.iterator();
		ComparablePoint p;
		while (it.hasNext()) {
			p = it.next();
			boundingRectangle.x = Math.min(p.x, boundingRectangle.x);
			boundingRectangle.y = Math.min(p.y, boundingRectangle.y);
			boundingRectangle.width = Math.max(p.x, boundingRectangle.width);
			boundingRectangle.height = Math.max(p.y, boundingRectangle.height);
		}
		boundingRectangle.width -= boundingRectangle.x - 1;
		boundingRectangle.height -= boundingRectangle.y - 1;
	}

	@Override
	public boolean isEmpty() {
		return this.selectedPoints.isEmpty();
	}

	/**
	 * Creates a shallow copy of this {@link DiscontinuousSelectionCollection}
	 * using a {@link HashSet} to represent points within the selection. The
	 * points themselves are not cloned. For extending classes, this operation
	 * is optional.
	 * 
	 * @throws CloneNotSupportedException
	 *@Override
	 */
	public abstract Object clone();

	@Override
	public boolean contains(int x, int y) {
		NULLPOINT.x = x;
		NULLPOINT.y = y;
		return contains(NULLPOINT);
	}

/**
	 * Determines whether the specified {@link ComparablePoint} resides within this
	 * selection.  Equivalent to calling {@link DiscontinuousSelectionCollection#contains(int, int)},
	 * this method is for convenience.
	 * @see {@link DiscontinuousSelectionCollection#contains(int, int)
	 * @param p specified point to test if residing within selection
	 * @return whether specified point resides within selection
	 */
	public boolean contains(ComparablePoint p) {
		return this.selectedPoints.contains(p);
	}

	@Override
	public boolean add(Selection other) {
		// if (other instanceof DiscontinuousSelection) {
		// DiscontinuousSelection selection = (DiscontinuousSelection) other;
		// boolean valid = selectedPoints.addAll(selection.selectedPoints);
		// updateBoundingRectangle();
		// return valid;
		// } else {
		Iterator<ComparablePoint> it = other.iterator();
		ComparablePoint p;
		while (it.hasNext()) {
			p = it.next();
			this.selectedPoints.add(p);
		}
		updateBoundingRectangle();
		return true;
		// }
	}

	@Override
	public boolean intersect(Selection other) {
		Rectangle cloneRect = (Rectangle) boundingRectangle.clone();
		cloneRect.x = Math.max(cloneRect.x, 0);
		cloneRect.y = Math.max(cloneRect.y, 0);
		Rectangle intersection = other.getBoundingRectangle().intersection(
				cloneRect);
		intersection.width++;
		intersection.height++;
		// the two selectin's bounding rectangle's intersect
		if (!intersection.isEmpty()) {
			Iterator<ComparablePoint> areaIT = new RectangularIterator(
					intersection);
			ComparablePoint p;
			HashSet<ComparablePoint> points = new HashSet<ComparablePoint>();
			// are points within this intersection contained in both
			// selections?
			while (areaIT.hasNext()) {
				p = areaIT.next();
				if (other.contains(p.x, p.y) && this.contains(p)) {
					points.add(p);
				}
			}
			// some points within selections overlap
			if (!points.isEmpty()) {
				this.selectedPoints.clear();
				this.selectedPoints = points;
				return true;
			}
			// was intersection of bounding rectangles but no points overlap
			else {
				return false;
			}
		}
		// intersection was empty, no modification
		else {
			return false;
		}
	}

	@Override
	public boolean subtract(Selection other) {
		boolean altered = false;
		if (other instanceof DiscontinuousSelectionCollection) {
			DiscontinuousSelectionCollection selection = (DiscontinuousSelectionCollection) other;
			altered = selectedPoints.removeAll(selection.selectedPoints);
		} else {
			Iterator<ComparablePoint> it = other.iterator();
			ComparablePoint p;
			while (it.hasNext()) {
				p = it.next();
				if (!altered) {
					altered = selectedPoints.remove(p);
				} else {
					selectedPoints.remove(p);
				}
			}
		}
		updateBoundingRectangle();
		return altered;
	}

	@Override
	public Iterator<ComparablePoint> iterator() {
		return selectedPoints.iterator();
	}

	@Override
	public Iterator<ComparablePoint> iterator(Rectangle rectangle) {
		Rectangle cloneRect = (Rectangle) boundingRectangle.clone();
		cloneRect.x = Math.max(cloneRect.x, 0);
		cloneRect.y = Math.max(cloneRect.y, 0);
		Rectangle intersection = rectangle.intersection(cloneRect);
		intersection.width++;
		intersection.height++;
		Set<ComparablePoint> points = new HashSet<ComparablePoint>();
		// the two selectin's bounding rectangle's intersect
		if (!intersection.isEmpty()) {
			Iterator<ComparablePoint> areaIT = new RectangularIterator(
					intersection);
			ComparablePoint p;
			// are points within this intersection contained in both
			// selections?
			while (areaIT.hasNext()) {
				p = areaIT.next();
				if (this.contains(p)) {
					points.add(p);
				}
			}
		}
		return points.iterator();
	}

	@Override
	public Selection addition(Selection other) {
		DiscontinuousSelectionCollection selection = (DiscontinuousSelectionCollection) this
				.clone();
		Iterator<ComparablePoint> it = other.iterator();
		while (it.hasNext()) {
			selection.addPoint(it.next());
		}
		return selection;
	}

	@Override
	public Selection subtraction(Selection other) {
		if (this.boundingRectangle.intersects(other.getBoundingRectangle())) {
			Rectangle intersection = boundingRectangle.intersection(other
					.getBoundingRectangle());
			DiscontinuousSelectionCollection selection = (DiscontinuousSelectionCollection) this
					.clone();
			Iterator<ComparablePoint> it = this.iterator(intersection);
			ComparablePoint p;
			while (it.hasNext()) {
				p = it.next();
				if (this.contains(p.x, p.y) && !other.contains(p.x, p.y)) {
					selection.addPoint(p);
				}
			}
			return selection;
		} else {
			return (DiscontinuousSelectionCollection) this.clone();
		}
	}

	@Override
	public Selection intersection(Selection other) {
		DiscontinuousSelectionCollection selection = (DiscontinuousSelectionCollection) this
				.clone();
		selection.clearSelection();
		Iterator<ComparablePoint> it = iterator();
		ComparablePoint p;
		while (it.hasNext()) {
			p = it.next();
			if (other.contains(p.x, p.y)) {
				selection.addPoint(p);
			}
		}
		return selection;
	}

	@Override
	public boolean intersects(Selection other) {
		if (this.boundingRectangle.intersects(other.getBoundingRectangle())) {
			Iterator<? extends Point> it = this.iterator();
			Point p;
			while (it.hasNext()) {
				p = it.next();
				if (other.contains(p.x, p.y)) {
					return true;
				}
			}
			return false;
		}
		return false;
	}
}
